﻿//Copyright 2011 - Jonathan Allen

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using Granite.Eventing;
using System.Collections.Specialized;
using Granite.ComponentModel;
using System.Diagnostics.Contracts;

namespace Granite.Collections
{


	/// <summary>
	/// This is an ObservableCollection with a read-only wrapper and support for weak events.
	/// This will use weak events to listen to objects implementing INotifyPropertyChangedWeak.
	/// This will use normal events to listen to objects implementing INotifyPropertyChanged.
	/// </summary>
	/// <typeparam name="T">The type of elements in the collection.</typeparam>
	public class ExtendedObservableCollection<T> : ObservableCollection<T>, INotifyCollectionChangedWeak, INotifyPropertyChangedWeak, INotifyItemPropertyChangedWeak
	{
		//These are created on demand.
		ExtendedReadOnlyObservableCollection<T> m_ReadOnlyWrapper;
		CollectionChangedEventManager m_CollectionChangeEventManager;
		PropertyChangedEventManager m_PropertyChangedEventManager;
		ItemPropertyChangedEventManager m_ItemPropertyChangedEventManager;
		IListener<PropertyChangedEventArgs> m_ItemPropertyChanged;


		event RelayedEventHandler<PropertyChangedEventArgs> m_ItemPropertyChangedEvent;

		/// <summary>
		/// When someone attaches to the ItemPropertyChanged event this is set to true and we start listening for change notifications.
		/// </summary>
		bool m_ListeningToItemEvents;


		/// <summary>
		/// This type safe event fires after an item is added to the collection no matter how it is added.
		/// </summary>
		/// <remarks>Triggered by InsertItem and SetItem</remarks>
		public event EventHandler<ItemEventArgs<T>> ItemAdded;


		/// <summary>
		/// This type safe event fires after an item is removed from the collection no matter how it is removed.
		/// </summary>
		/// <remarks>Triggered by SetItem, RemoveItem, and ClearItems</remarks>
		public event EventHandler<ItemEventArgs<T>> ItemRemoved;

		/// <summary>
		/// This fires after an item is removed from the collection no matter how it is removed.
		/// </summary>
		/// <param name="item"></param>
		/// <remarks>Triggered by SetItem, RemoveItem, and ClearItems.</remarks>
		protected virtual void OnItemAdded(T item)
		{
			if (ItemAdded != null)
				ItemAdded(this, new ItemEventArgs<T>(item));

			if (m_ListeningToItemEvents)
			{
				if (item is INotifyPropertyChangedWeak)
					((INotifyPropertyChangedWeak)item).AddHandler(m_ItemPropertyChanged);
				else if (item is INotifyPropertyChanged)
					((INotifyPropertyChanged)item).PropertyChanged += OnItemPropertyChanged;
			}
		}

		/// <summary>
		/// This fires after an item is removed from the collection no matter how it is removed.
		/// </summary>
		/// <param name="item"></param>
		/// <remarks>Triggered by SetItem, RemoveItem, and ClearItems.</remarks>
		protected virtual void OnItemRemoved(T item)
		{
			if (ItemRemoved != null)
				ItemRemoved(this, new ItemEventArgs<T>(item));

			if (m_ListeningToItemEvents)
			{
				if (item is INotifyPropertyChangedWeak)
					((INotifyPropertyChangedWeak)item).RemoveHandler(m_ItemPropertyChanged);
				if (item is INotifyPropertyChanged)
					((INotifyPropertyChanged)item).PropertyChanged -= OnItemPropertyChanged;
			}

		}

		/// <summary>
		/// This just exposes the INotifyPropertyChanged.PropertyChanged from the base class so you don't have to cast to get to it.
		/// </summary>
		public new event PropertyChangedEventHandler PropertyChanged
		{
			add { base.PropertyChanged += value; }
			remove { base.PropertyChanged -= value; }
		}

		/// <summary>
		/// Removes all elements from the collection.
		/// </summary>
		protected override void ClearItems()
		{
			var temp = this.ToList();
			base.ClearItems();
			foreach (var item in temp)
				OnItemRemoved(item);
		}

		/// <summary>
		/// Replaces the element at the specified index.
		/// </summary>
		/// <param name="index">The zero-based index of the element to replace.</param>
		/// <param name="item">The new value for the element at the specified index.</param>
		/// <remarks>Do NOT invoke this method directly. This may be overridden to provide additional validation before an item is added to the collection.</remarks>
		protected override void SetItem(int index, T item)
		{
			if (index < 0)
				throw ExceptionUtilities.ArgumentOutOfRangeException("index", index, "index must be >= 0");
			if (index >= Count)
				throw ExceptionUtilities.ArgumentOutOfRangeException("index", index, "index must be < Count");
			Contract.EndContractBlock();

			T temp = base[index];
			base.SetItem(index, item);
			OnItemRemoved(temp);
			OnItemAdded(item);
		}

		/// <summary>
		/// Inserts an item into the collection at the specified index.
		/// </summary>
		/// <param name="index">The zero-based index at which item should be inserted.</param>
		/// <param name="item">The object to insert.</param>
		/// <remarks>Do NOT invoke this method directly. This may be overridden to provide additional validation before an item is added to the collection.</remarks>
		protected override void InsertItem(int index, T item)
		{
			base.InsertItem(index, item);
			OnItemAdded(item);
		}

		/// <summary>
		/// Removes the element at the specified index of the <see cref="T:System.Collections.ObjectModel.Collection`1" />.
		/// </summary>
		/// <param name="index">
		/// The zero-based index of the element to remove.
		/// </param>
		/// <exception cref="T:System.ArgumentOutOfRangeException">
		/// <paramref name="index" /> is less than zero -or-<paramref name="index" /> is equal to or greater than <see cref="P:System.Collections.ObjectModel.Collection`1.Count" />.
		/// </exception>
		/// <remarks>Do NOT invoke this method directly. This may be overridden to provide additional validation before an item is removed to the collection.</remarks>
		protected override void RemoveItem(int index)
		{
			if (index < 0)
				throw ExceptionUtilities.ArgumentOutOfRangeException("index", index, "index must be >= 0");
			if (index >= Count)
				throw ExceptionUtilities.ArgumentOutOfRangeException("index", index, "index must be < Count");
			Contract.EndContractBlock();

			T temp = base[index];
			base.RemoveItem(index);
			OnItemRemoved(temp);
		}

		/// <summary>
		/// Initializes a new instance of the ImprovedObservableCollection class.
		/// </summary>
		public ExtendedObservableCollection()
		{
		}

		/// <summary>
		/// Initializes a new instance of the ImprovedObservableCollection class that contains elements copied from the specified list.
		/// </summary>
		/// <param name="list"></param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
		public ExtendedObservableCollection(List<T> list)
		{
			if (list != null)
				AddRange(list);
		}

		/// <summary>
		/// Initializes a new instance of the ImprovedObservableCollection class that contains elements copied from the specified collection.
		/// </summary>
		/// <param name="collection"></param>
		public ExtendedObservableCollection(IEnumerable<T> collection)
		{
			if (collection != null)
				AddRange(collection);
		}

		/// <summary>
		/// Raises the <see cref="E:System.Collections.ObjectModel.ObservableCollection`1.PropertyChanged" /> event with the provided arguments.
		/// </summary>
		/// <param name="propertyName">
		/// Property that is being changed.
		/// </param>
		protected void OnPropertyChanged(string propertyName)
		{
			OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
		}

		/// <summary>
		/// Returns a read-only wrapper around this collection. 
		/// </summary>
		/// <remarks>
		/// If subclassing this class then it may be useful to shadow ReadOnlyWrapper method 
		/// with one that returns a subclass of ExtendedReadOnlyObservableCollection.
		/// </remarks>
		public ExtendedReadOnlyObservableCollection<T> ReadOnlyWrapper
		{
			get
			{
				Contract.Ensures(Contract.Result<ExtendedReadOnlyObservableCollection<T>>() != null);

				if (m_ReadOnlyWrapper == null)
					m_ReadOnlyWrapper = new ExtendedReadOnlyObservableCollection<T>(this);

				return m_ReadOnlyWrapper;
			}
		}

		/// <summary>
		/// Adds a weak event handler
		/// </summary>
		/// <param name="eventHandler"></param>
		public void AddHandler(IListener<NotifyCollectionChangedEventArgs> eventHandler)
		{
			if (eventHandler == null)
				throw new ArgumentNullException("eventHandler", "eventHandler is null.");
			Contract.EndContractBlock();

			if (m_CollectionChangeEventManager == null)
				m_CollectionChangeEventManager = new CollectionChangedEventManager(this);

			m_CollectionChangeEventManager.AddHandler(eventHandler);
		}

		/// <summary>
		/// Removes a weak event handler
		/// </summary>
		/// <param name="eventHandler"></param>
		public void RemoveHandler(IListener<NotifyCollectionChangedEventArgs> eventHandler)
		{
			if (eventHandler == null)
				throw new ArgumentNullException("eventHandler", "eventHandler is null.");
			Contract.EndContractBlock();

			if (m_CollectionChangeEventManager == null)
				return;

			m_CollectionChangeEventManager.RemoveHandler(eventHandler);
		}


		/// <summary>
		/// Adds a weak event handler
		/// </summary>
		/// <param name="eventHandler"></param>
		public void AddHandler(IListener<PropertyChangedEventArgs> eventHandler)
		{
			if (eventHandler == null)
				throw new ArgumentNullException("eventHandler", "eventHandler is null.");
			Contract.EndContractBlock();

			if (m_PropertyChangedEventManager == null)
				m_PropertyChangedEventManager = new PropertyChangedEventManager(this);

			m_PropertyChangedEventManager.AddHandler(eventHandler);
		}

		/// <summary>
		/// Removes a weak event handler
		/// </summary>
		/// <param name="eventHandler"></param>
		public void RemoveHandler(IListener<PropertyChangedEventArgs> eventHandler)
		{
			if (eventHandler == null)
				throw new ArgumentNullException("eventHandler", "eventHandler is null.");
			Contract.EndContractBlock();

			if (m_PropertyChangedEventManager != null)
				m_PropertyChangedEventManager.RemoveHandler(eventHandler);
		}

		/// <summary>
		/// Adds a weak event handler
		/// </summary>
		/// <param name="eventHandler"></param>
		public void AddHandler(IListener<RelayedEventArgs<PropertyChangedEventArgs>> eventHandler)
		{
			if (eventHandler == null)
				throw new ArgumentNullException("eventHandler", "eventHandler is null.");
			Contract.EndContractBlock();

			if (m_ItemPropertyChangedEventManager == null)
			{
				m_ItemPropertyChangedEventManager = new ItemPropertyChangedEventManager(this);
				ListenToEvents();
			}

			Contract.Assume(m_ItemPropertyChangedEventManager != null);

			m_ItemPropertyChangedEventManager.AddHandler(eventHandler);
		}

		/// <summary>
		/// Removes a weak event handler
		/// </summary>
		/// <param name="eventHandler"></param>
		public void RemoveHandler(IListener<RelayedEventArgs<PropertyChangedEventArgs>> eventHandler)
		{
			if (eventHandler == null)
				throw new ArgumentNullException("eventHandler", "eventHandler is null.");
			Contract.EndContractBlock();

			if (m_ItemPropertyChangedEventManager != null)
				m_ItemPropertyChangedEventManager.RemoveHandler(eventHandler);
		}

		/// <summary>
		/// Adds a list of values to this collection
		/// </summary>
		/// <param name="list"></param>
		public void AddRange(IEnumerable<T> list)
		{
			if (list == null)
				throw new ArgumentNullException("list", "list is null.");
			Contract.EndContractBlock();

			foreach (var item in list)
				Add(item);
		}


		void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (m_ItemPropertyChangedEvent != null)
				m_ItemPropertyChangedEvent(this, RelayedEventArgs.Create(sender, e));
		}

		/// <summary>
		/// This enables the ItemPropertyChanged events. 
		/// </summary>
		private void ListenToEvents()
		{
			if (m_ListeningToItemEvents)
				return;

			m_ItemPropertyChanged = new Listener<PropertyChangedEventArgs>(OnItemPropertyChanged);
			foreach (var item in this.OfType<INotifyPropertyChanged>())
			{
				Contract.Assume(item != null);

				if (item is INotifyPropertyChangedWeak)
					((INotifyPropertyChangedWeak)item).AddHandler(m_ItemPropertyChanged);
				else
					item.PropertyChanged += OnItemPropertyChanged;
			}


			m_ListeningToItemEvents = true;
		}


		/// <summary>
		/// Use this event to listen for changes to properties on items contained by this collection without having to explicitly attach an event handler to each item.
		/// </summary>
		public event RelayedEventHandler<PropertyChangedEventArgs> ItemPropertyChanged
		{
			add
			{
				ListenToEvents();
				m_ItemPropertyChangedEvent += value;
			}
			remove { m_ItemPropertyChangedEvent -= value; }
		}

	}



}
